﻿//-----------------------------------------------------------------------
// <copyright file="WebPrivateMessageSource.cs" company="AFS">
// Copyright (c) AFS. All rights reserved.
//
//    This file is part of Chatbox.
//
//    Chatbox is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Lesser General Public License as published by
//    the Free Software Foundation, either version 3 of the License, or
//    any later version.
//
//    Chatbox is distributed in the hope that it will be useful,
//    but WITHOUT ANY WARRANTY; without even the implied warranty of
//    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//    GNU Lesser General Public License for more details.
//
//    You should have received a copy of the GNU Lesser General Public License
//    along with Chatbox. If not, see http://www.gnu.org/copyleft/lesser.html
// </copyright>
//----------------------------------------------------------------------- 

namespace ChatBox.Engine.Web
{
    using System;
    using System.IO;
    using System.Linq;
    using System.Net;
    using System.Text;
    using System.Threading;
    using System.Xml.Linq;

    using ChatBox.DomainObjects.ResultData;
    using ChatBox.DomainObjects.XmlFeedObjects;
    using ChatBox.Interfaces;
    using ChatBox.Utility;

    /// <summary>
    /// class to retrieve private messages
    /// </summary>
    public sealed class WebPrivateMessageSource : BaseWebDataSource, IPrivateChatDataSource, IDisposable
    {
        #region data

        /// <summary>
        /// Source for logon data
        /// </summary>
        private ILogOnSource logOnSource;

        /// <summary>
        /// true if the first message data has not yet been received
        /// </summary>
        private bool firstMessageData = true;

        /// <summary>
        /// The last message id received
        /// </summary>
        private int lastMessageId;

        #endregion

        #region constructor

        /// <summary>
        /// Initializes a new instance of the WebPrivateMessageSource class 
        /// </summary>
        /// <param name="settingsRepository">Settings data source</param>
        /// <param name="logger">interface to error logging</param>
        /// <param name="logOnSource">The source of logon details</param>
        public WebPrivateMessageSource(
            IServerSettingsRepository settingsRepository, 
            ILogger logger,
            ILogOnSource logOnSource)
        {
            this.Logger = logger;
            this.ServerSettings = settingsRepository.ReadSettings();
            this.logOnSource = logOnSource;
            this.logOnSource.Completed += this.LogOnSourceCompleted;
        }

        #endregion

        /// <summary>
        /// tell listeners that new messages have arrived
        /// </summary>        
        public event EventHandler<EventArgs<ChatDataResult>> DataReceived;
        
        #region public

        /// <summary>
        /// Start going
        /// </summary>
        public void Start()
        {
            if (!this.IsStarted)
            {
                this.IsStarted = true;

                // get the login
                if (this.logOnSource.LogOnCookie != null)
                {
                    this.StartThread();                    
                }
                else
                {
                    this.logOnSource.GetLogOn();
                }
            }
        }

        /// <summary>
        /// IDisposable member
        /// </summary>
        public void Dispose()
        {
            this.DisposeInternal();
            GC.SuppressFinalize(this);
        }

        #endregion

        #region protected

        /// <summary>
        /// The thread has called back with an error
        /// </summary>
        /// <param name="ex">the exception</param>
        protected override void ReadThreadOnError(Exception ex)
        {
            ChatDataResult failResult = new ChatDataResult(false, ex);
            this.SendMessageData(failResult);
        }

        #endregion

        #region private

        /// <summary>
        /// Callback from the login source
        /// </summary>
        /// <param name="sender">the event sender</param>
        /// <param name="e">the event params</param>
        private void LogOnSourceCompleted(object sender, EventArgs<LogOnResult> e)
        {
            if (this.logOnSource.LogOnCookie != null && this.IsStarted)
            {
                this.StartThread();
            }
        }

        /// <summary>
        /// Start the message read thread 
        /// </summary>
        private void StartThread()
        {
            // make the thread
            this.ReadThread = new Thread(this.ThreadLoop);
            this.ReadThread.IsBackground = true;
            this.ReadThread.Name = "Chatbox private message read thread";

            // The thread must poll for messages at intervals, 
            // but to start it must not sleep at all until interfaces and initial messages are here
            this.SleepBetweenReads = false;
            this.ReadInterval = this.ServerSettings.PrivateChatRefreshInterval;

            // Start the private message reading thread
            this.ReadThread.Start();
        }

        /// <summary>
        /// The thread main loop
        /// </summary>
        private void ThreadLoop()
        {
            while (!this.ThreadTerminated)
            {
                string data = this.DownloadData();

                if (!string.IsNullOrEmpty(data))
                {
                    try
                    {
                        TextReader reader = new StringReader(data);
                        XDocument doc = XDocument.Load(reader);
                        this.ReadThreadDataReceived(doc);
                    }
                    catch (Exception ex)
                    {
                        // report the error, carry on 
                        this.HandleXmlParseException(ex);
                    }
                }

                if (!this.ThreadTerminated && this.SleepBetweenReads)
                {
                    Thread.Sleep(this.ReadInterval);
                }
            }
        }

        /// <summary>
        /// download data from the web server
        /// </summary>
        /// <returns>the data downloaded</returns>
        private string DownloadData()
        {
            try
            {
                HttpWebRequest webRequest = (HttpWebRequest) WebRequest.Create(this.GetReadUrl());

                webRequest.UserAgent = EngineConstants.UserAgentString;
                webRequest.CookieContainer = new CookieContainer();
                webRequest.CookieContainer.Add(this.logOnSource.LogOnCookie);
                webRequest.KeepAlive = false;
                webRequest.Method = "GET";

                HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse();
                StreamReader responseStream = new StreamReader(webResponse.GetResponseStream(), Encoding.UTF8);
                return responseStream.ReadToEnd();
            }
            catch (Exception ex)
            {
                // report the error, return nothing
                this.HandleWebException(ex);
                return string.Empty;
            }
        }

        /// <summary>
        /// The thread had called back with data in an XDocument
        /// </summary>
        /// <param name="document">the data received</param>
        private void ReadThreadDataReceived(XDocument document)
        {
            ChatDataResult messagesData = document.ToMessages(false);

            // once we have first messages, pause between polling the server
            this.SleepBetweenReads = true;

            int messageCount = messagesData.Messages.Count();
            if (messageCount == 1)
            {
                messagesData.Message = "1 private message received";
            }
            else if (messageCount > 1)
            {
                messagesData.Message = messageCount + " private messages received";
            }

            int maxMessageReceived = messagesData.Messages.MaxMessageId();

            if (maxMessageReceived > this.lastMessageId)
            {
                this.lastMessageId = maxMessageReceived;
            }

            // notify listeners if anything has changed
            // - this is the first packet of data
            // - there are new messages
            // - topic has changed from the last time
            if (this.firstMessageData || messagesData.Messages.HasItems())
            {
                this.firstMessageData = false;
                this.SendMessageData(messagesData);
            }
        }

        /// <summary>
        /// Construct a URL to read for the current state
        /// </summary>
        /// <returns>the complete URl</returns>
        private Uri GetReadUrl()
        {
            string result = this.ServerSettings.Server + "/node/ticker/Universal+Message+XML+Ticker?do_the_right_thing=1&nosort=1&for_node=me";
            if (this.lastMessageId > 0)
            {
                result += "&msglimit=" + this.lastMessageId;
            }

            return new Uri(result);
        }

        /// <summary>
        /// Notify listeners of message data
        /// </summary>
        /// <param name="messageData">Message data to send</param>
        private void SendMessageData(ChatDataResult messageData)
        {
            messageData.ServerDataType = ServerDataType.PrivateMessages;
            this.SetSequence(messageData);

            if (this.DataReceived != null)
            {
                this.DataReceived(this, new EventArgs<ChatDataResult>(messageData));
            }
        }

        /// <summary>
        /// Shut down the message read thread and dispose of resources
        /// </summary>
        private void DisposeInternal()
        {
            if (this.ReadThread != null)
            {
                this.ThreadTerminated = true;
                this.ReadThread = null;
            }
       }

        #endregion
    }
}